package com.idanchuang.support.mybatis.crypt;

import com.idanchuang.support.mybatis.crypt.annotation.CryptEntity;
import com.idanchuang.support.mybatis.crypt.config.CryptConfigBean;
import com.idanchuang.support.mybatis.crypt.resolver.*;
import com.idanchuang.support.mybatis.crypt.util.CryptUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 加解密插件
 *
 * @author yjy
 */
@Intercepts(value = {@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class,
                        BoundSql.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class CryptInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(CryptInterceptor.class);

    private static CryptConfigBean cryptConfigBean;

    private static final String DEFAULT_ID_FIELD_NAME = "id";

    /**
     * 存储源对象和新对象
     */
    public static final ConcurrentHashMap<String, Object[]> OLD_AND_NEW_OBJ_MAP = new ConcurrentHashMap<>();

    /**
     * 需加解密处理方法的信息
     */
    private static final ConcurrentHashMap<String, MethodCryptMetadata> METHOD_ENCRYPT_MAP = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String processId = UUID.randomUUID().toString();
        try {
            CryptConfigBean cryptConfigBean = getCryptConfigBean();
            if (cryptConfigBean != null && cryptConfigBean.isEnable()) {
                Object[] args = invocation.getArgs();
                MappedStatement mappedStatement = (MappedStatement) args[0];
                Method runningMethod = getMethod(mappedStatement.getId());
                MethodCryptMetadata methodCryptMetadata = getCachedMethodCryptMetaData(mappedStatement, runningMethod);
                if (cryptConfigBean.isEnableEncrypt()) {
                    Object originObj = args[1];
                    Object encryptObj = methodCryptMetadata.encrypt(originObj);
                    OLD_AND_NEW_OBJ_MAP.put(processId, new Object[]{originObj, encryptObj});
                    args[1] = encryptObj;
                }
                Object returnValue = invocation.proceed();
                this.ifInsertReturnId(mappedStatement.getSqlCommandType(), methodCryptMetadata.methodEncryptResolver, processId);
                if (cryptConfigBean.isEnableDecrypt()) {
                    returnValue = methodCryptMetadata.decrypt(returnValue);
                }
                return returnValue;
            } else {
                return invocation.proceed();
            }
        } finally {
            OLD_AND_NEW_OBJ_MAP.remove(processId);
        }
    }

    private void ifInsertReturnId(SqlCommandType currentCommandType, MethodEncryptResolver methodEncryptResolver, String processId) {
        boolean isInsert = Objects.equals("INSERT", currentCommandType.name());
        if (isInsert) {
            returnIdToSourceBean(methodEncryptResolver, processId);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    private MethodCryptMetadata getCachedMethodCryptMetaData(MappedStatement mappedStatement, Method runningMethod) {
        return METHOD_ENCRYPT_MAP.computeIfAbsent(mappedStatement.getId(),
                id -> new MethodCryptMetadataBuilder(runningMethod).build());
    }

    public static CryptConfigBean getCryptConfigBean() {
        return cryptConfigBean != null ? cryptConfigBean : (cryptConfigBean = CryptConfigBean.INSTANCE);
    }

    /**
     * 根据statementId获取本次运行的方法
     *
     * @return Method
     */
    private Method getMethod(String statementId) throws ClassNotFoundException {
        final Class clazz = Class.forName(statementId.substring(0, statementId.lastIndexOf(".")));
        final String methodName = statementId.substring(statementId.lastIndexOf(".") + 1);
        Method method = null;
        for (Method m : clazz.getMethods()) {
            if (m.getName().equals(methodName)) {
                method = m;
                if (!method.isDefault()) {
                    break;
                }
            }
        }
        return method;
    }

    /**
     * 修复selectKey无法赋值给源对象(源对象被clone,因为需要避免重复加密)
     */
    private static void returnIdToSourceBean(MethodEncryptResolver methodEncryptResolver, String processId) {
        Object[] objs = OLD_AND_NEW_OBJ_MAP.get(processId);
        if (objs != null && objs.length == 2) {
            Object sourceObj = objs[0];
            Object cloneObj = objs[1];
            if (sourceObj instanceof Map && cloneObj instanceof Map) {
                if (methodEncryptResolver instanceof AnnotationMethodEncryptResolver) {
                    AnnotationMethodEncryptResolver annotationMethodEncryptResolver = (AnnotationMethodEncryptResolver) methodEncryptResolver;
                    List<MethodAnnotationEncryptParameter> parameters = annotationMethodEncryptResolver.getMethodAnnotationEncryptParameterList();
                    for (MethodAnnotationEncryptParameter parameter : parameters) {
                        String paramName = parameter.getParamName();
                        if (paramName != null) {
                            Map paramMap = (Map) sourceObj;
                            Map cloneMap = (Map) cloneObj;

                            Object sourceObj1 = paramMap.get(paramName);
                            Object cloneObj1 = cloneMap.get(paramName);
                            if (sourceObj1 instanceof List && cloneObj1 instanceof List) {
                                List<Object> sourceList = (List<Object>) sourceObj1;
                                List<Object> cloneList = (List<Object>) cloneObj1;
                                for (int i = 0; i < sourceList.size(); i++) {
                                    fillOriginBeanId(sourceList.get(i), cloneList.get(i));
                                }
                            } else {
                                fillOriginBeanId(sourceObj1, cloneObj1);
                            }

                        }
                    }
                }
            } else if (sourceObj instanceof List && cloneObj instanceof List) {
                List<Object> sourceList = (List<Object>) sourceObj;
                List<Object> cloneList = (List<Object>) cloneObj;
                for (int i = 0; i < sourceList.size(); i++) {
                    fillOriginBeanId(sourceList.get(i), cloneList.get(i));
                }
            } else {
                fillOriginBeanId(sourceObj, cloneObj);
            }
        }
    }

    /**
     * id回写到源对象
     * @param sourceObj 源对象
     * @param cloneObj 克隆的加密对象
     */
    private static void fillOriginBeanId(Object sourceObj, Object cloneObj) {
        if (sourceObj == null || cloneObj == null) {
            return;
        }
        Class<?> sourceClass = sourceObj.getClass();
        Class<?> cloneClass = cloneObj.getClass();
        // 非实体对象则不做id回写
        if (CryptUtil.inIgnoreClass(sourceClass)) {
            return;
        }
        String idFieldName = findIdFieldName(sourceClass);
        if (idFieldName == null || idFieldName.isEmpty()) {
            return;
        }
        try {
            Field idField = getIdFieldFromClass(sourceClass, idFieldName);
            idField.setAccessible(Boolean.TRUE);
            Object cloneObjFieldIdVal = idField.get(cloneObj);
            if (cloneObjFieldIdVal == null) {
                return;
            }
            idField.set(sourceObj, cloneObjFieldIdVal);
        } catch (NoSuchFieldException e) {
            log.warn("fillOriginBeanId cancel, cause NoSuchFieldException, sourceClass: {}, cloneClass: {}, " +
                            "idFieldName: {}",
                    sourceClass, cloneClass, idFieldName);
        } catch (Exception e) {
            log.error("fillOriginBeanId failed.", e);
        }
    }

    /**
     * 根据实体类与id字段名找到字段
     * @param cls 实体类
     * @param idFieldName id字段名
     * @return 字段
     * @throws NoSuchFieldException e
     */
    private static Field getIdFieldFromClass(Class<?> cls, String idFieldName) throws NoSuchFieldException {
        try {
            return cls.getDeclaredField(idFieldName);
        } catch (NoSuchFieldException e) {
            Class<?> superClass = cls.getSuperclass();
            if (superClass != null && !superClass.equals(Object.class)) {
                return getIdFieldFromClass(superClass, idFieldName);
            }
            throw e;
        }
    }

    /**
     * 找到id字段名
     * @param cls 实体类
     * @return id字段名
     */
    private static String findIdFieldName(Class<?> cls) {
        if (cls == null || cls.equals(Object.class)) {
            return DEFAULT_ID_FIELD_NAME;
        }
        if (cls.isAnnotationPresent(CryptEntity.class)) {
            CryptEntity cryptEntity = cls.getAnnotation(CryptEntity.class);
            return cryptEntity.idField();
        }
        return findIdFieldName(cls.getSuperclass());
    }

}
